package top.gokzzz.common.utils;

import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.Optional;

/**
 * 文件 工具類
 *
 * @author Gok
 * @version 1.2
 */
public class FileUtil {

    /**
     * GB 大小
     */
    private static final int GB = 1024 * 1024 * 1024;

    /**
     * MB 大小
     */
    private static final int MB = 1024 * 1024;

    /**
     * KB 大小
     */
    private static final int KB = 1024;

    /**
     * 格式化小数
     */
    private static final DecimalFormat DF = new DecimalFormat("0.00");

    /**
     * 系统临时目录
     * <br>
     * windows 包含路径分割符，但 Linux 不包含,
     * 在 windows \\==\ 前提下，
     * 为安全起见 同意拼装 路径分割符，
     * <pre>
     *       java.io.tmpdir
     *       windows : C:\Users/xxx\AppData\Local\Temp\
     *       linux: /temp
     * </pre>
     */
    public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;

    /**
     * 默認緩存大小（小） 8192
     */
    public static final int DEFAULT_BUFFER_SIZE = 2 << 12;

    /**
     * 默認緩存大小（中） 16384
     */
    public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 2 << 13;

    /**
     * 默認緩存大小（大） 32768 <br/>
     * 较大的緩存区（32KB）可减少磁盘 I/O 次数，尤其对大文件更高效。<br/>
     * 讀取次數 = 文件大小 / 緩存区大小<br/>
     */
    public static final int DEFAULT_LARGE_BUFFER_SIZE = 2 << 14;

    /**
     * 獲取 MIME 類型
     *
     * @param filename 文件名
     * @return {@link String } MIME 類型，找不到則返回 null
     */
    public static String getMIMEType(String filename) {
        Optional<MediaType> mediaType = MediaTypeFactory.getMediaType(filename.toLowerCase());
        if (mediaType.isPresent()) {
            MediaType type = mediaType.get();
            return type.toString();
        } else {
            return null;
        }
    }

    /**
     * 獲取文件後綴名
     *
     * @param fileName 文件名
     * @return {@link String } 文件的後綴名，如果沒有後綴名則返回空字符串
     */
    public static String extName(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return "";
        }

        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1 || lastDotIndex == fileName.length() - 1) {
            return "";
        }

        return fileName.substring(lastDotIndex + 1);
    }

    /**
     * 判斷文件是否為圖片類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是圖片類型返回 true，否則返回 false
     */
    public static boolean isImage(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("jpg") || ext.equals("jpeg") || ext.equals("png") ||
                ext.equals("gif") || ext.equals("bmp") || ext.equals("tiff") ||
                ext.equals("webp");
    }


    /**
     * 判斷文件是否為 Excel 類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是 Excel 類型返回 true，否則返回 false
     */
    public static boolean isExcel(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("xls") || ext.equals("xlsx");
    }

    /**
     * 判斷文件是否為 Word 類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是 Word 類型返回 true，否則返回 false
     */
    public static boolean isWord(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("doc") || ext.equals("docx");
    }

    /**
     * 判斷文件是否為 PDF 類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是 PDF 類型返回 true，否則返回 false
     */
    public static boolean isPdf(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("pdf");
    }

    /**
     * 判斷文件是否為視頻類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是視頻類型返回 true，否則返回 false
     */
    public static boolean isVideo(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("mp4") || ext.equals("avi") || ext.equals("mov") ||
                ext.equals("wmv") || ext.equals("mkv") || ext.equals("flv");
    }

    /**
     * 判斷文件是否為語音類型
     *
     * @param fileName 文件名
     * @return {@link boolean} 如果是語音類型返回 true，否則返回 false
     */
    public static boolean isAudio(String fileName) {
        String ext = extName(fileName).toLowerCase();
        return ext.equals("mp3") || ext.equals("wav") || ext.equals("aac") ||
                ext.equals("ogg") || ext.equals("flac");
    }


    /**
     * 根據文件大小轉為合適的對應單位字眼
     *
     * @param size 文件大小
     * @return {@link String }
     */
    public static String getSize(long size) {
        String resultSize;
        if (size / GB >= 1) {
            //如果當前 Byte 的值大於等於 1GB
            resultSize = DF.format(size / (float) GB) + "GB";
        } else if (size / MB >= 1) {
            //如果當前 Byte 的值大於等於 1MB
            resultSize = DF.format(size / (float) MB) + "MB";
        } else if (size / KB >= 1) {
            //如果當前 Byte 的值大於等於 1KB
            resultSize = DF.format(size / (float) KB) + "KB";
        } else {
            resultSize = size + "B   ";
        }
        return resultSize;
    }

    /**
     * ByteArrayOutputStream -> File
     *
     * @param outputStream 输出流
     * @return {@link File } 臨時文件
     * @throws IOException IOException
     */
    public static File outputStream2File(ByteArrayOutputStream outputStream) throws IOException {
        // 創建臨時文件（自動生成唯一文件名）
        File tempFile = File.createTempFile("G-TEMP-", ".tmp", new File(SYS_TEM_DIR));

        try (FileOutputStream fos = new FileOutputStream(tempFile)) {
            outputStream.writeTo(fos);
        }
        return tempFile;
    }

    /**
     * 刪除文件
     *
     * @param file 文件
     * @return boolean 是否刪除成功
     */
    public static boolean deleteFile(File file) {
        if (file != null && file.exists()) {
            return file.delete();
        }
        return false;
    }

    /**
     * 生成 MD5
     *
     * @param file 文件
     * @return {@link String } MD5
     * @throws IOException IOException
     */
    public static String generateMD5(File file) throws IOException {
        try (InputStream inputStream = Files.newInputStream(file.toPath())) {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int read;

            while ((read = inputStream.read(buffer)) != -1) {
                md.update(buffer, 0, read);
            }
            return bytes2Hex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 algorithm not available", e);
        }
    }

    /**
     * 傳統拷貝<br/>
     * 使用緩衝區，需多次內核態與用戶態數據拷貝
     *
     * @param input  輸入流
     * @param output 輸出流
     * @return int 複製的字節總數
     * @throws IOException IOException
     */
    public static int copy(InputStream input, OutputStream output) throws IOException {
        byte[] buffer = new byte[DEFAULT_LARGE_BUFFER_SIZE];
        int totalBytes = 0;
        int bytesRead;

        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead);
            totalBytes += bytesRead;
        }
        return totalBytes;
    }

    /**
     * NIO 的拷貝<br/>
     * 實現零拷貝，直接操作內核緩存區，適合要求性能高、文件大的處理方式
     *
     * @param input  輸入流
     * @param output 輸出流
     * @return long 複製的字節總數
     * @throws IOException IOException
     */
    public static long nioCopy(FileInputStream input, FileOutputStream output) throws IOException {
        try (FileChannel inChannel = input.getChannel();
             FileChannel outChannel = output.getChannel()) {
            return inChannel.transferTo(0, inChannel.size(), outChannel);
        }
    }

    /**
     * 字節轉為十六進制
     *
     * @param bytes 字節
     * @return {@link String } 十六進制
     */
    private static String bytes2Hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
